package com.gooluke.aop;

import com.gooluke.common.annotation.FixedDataSource;
import com.sun.istack.internal.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.aop.Advice;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.AbstractPointcutAdvisor;
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.aop.support.StaticMethodMatcher;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.Assert;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @author gooluke
 */
@Slf4j
public class DataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {

    private final Advice advice;
    private final Pointcut pointcut;

    public DataSourceAnnotationAdvisor(@NotNull DataSourceAnnotationInterceptor dataSourceAnnotationInterceptor) {
        if (dataSourceAnnotationInterceptor == null) {
            throw new NullPointerException("dataSourceAnnotationInterceptor is marked non-null but is null");
        } else {
            log.info("DataSourceAnnotationAdvisor init...");
            this.advice = dataSourceAnnotationInterceptor;
            this.pointcut = this.buildPointcut();
        }
    }

    @Override
    public Pointcut getPointcut() {
        return this.pointcut;
    }

    @Override
    public Advice getAdvice() {
        return this.advice;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        if (this.advice instanceof BeanFactoryAware) {
            ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
        }
    }

    private Pointcut buildPointcut() {
        Pointcut cpc = new AnnotationMatchingPointcut(FixedDataSource.class, true);
        Pointcut mpc = new AnnotationMethodPoint(FixedDataSource.class);
        return (new ComposablePointcut(cpc)).union(mpc);
    }

    private static class AnnotationMethodPoint implements Pointcut {
        private final Class<? extends Annotation> annotationType;

        public AnnotationMethodPoint(Class<? extends Annotation> annotationType) {
            Assert.notNull(annotationType, "Annotation type must not be null");
            this.annotationType = annotationType;
        }

        public ClassFilter getClassFilter() {
            return ClassFilter.TRUE;
        }

        public MethodMatcher getMethodMatcher() {
            return new AnnotationMethodMatcher(this.annotationType);
        }

        private static class AnnotationMethodMatcher extends StaticMethodMatcher {
            private final Class<? extends Annotation> annotationType;

            public AnnotationMethodMatcher(Class<? extends Annotation> annotationType) {
                this.annotationType = annotationType;
            }

            public boolean matches(Method method, Class<?> targetClass) {
                if (this.matchesMethod(method)) {
                    return true;
                } else if (Proxy.isProxyClass(targetClass)) {
                    return false;
                } else {
                    Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
                    return specificMethod != method && this.matchesMethod(specificMethod);
                }
            }

            private boolean matchesMethod(Method method) {
                return AnnotatedElementUtils.hasAnnotation(method, this.annotationType);
            }
        }
    }
}
